Выгрузим данные о календаре маркетинговых событий, новых пользователях, всех событиях новых пользователей и участниках тестов из csv-файлов в датафрейм и сохраним в переменные ab_project_marketing_events, final_ab_new_users, final_ab_events и final_ab_participants соответственно.
# загружаем библиотеки
import os
import pandas as pd
import datetime as dt
import plotly.express as px
from matplotlib import pyplot as plt
from plotly import graph_objects as go
from scipy import stats as st
import numpy as np
import math as mth
# для подгрузки данных будем использовать методы модуля os
def read_file(file_name):
path_1 = os.path.join('/datasets/', file_name)
path_2 = os.path.join( file_name)
if os.path.isfile(path_1):
df = pd.read_csv(path_1)
return df
elif os.path.isfile(path_2):
df = pd.read_csv(path_2)
return df
else:
print('File is not found')
# сохраним результат функции `read_file` в соответствующие переменные
ab_project_marketing_events = read_file('ab_project_marketing_events.csv')
final_ab_new_users = read_file('final_ab_new_users.csv')
final_ab_events = read_file('final_ab_events.csv')
final_ab_participants = read_file('final_ab_participants.csv')
Изучим данные датафрейма ab_project_marketing_events - календарь маркетинговых событий на 2020 год.
ab_project_marketing_events.head(5)
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
Таблица ab_project_marketing_events содержит следующую информацию:
name - название маркетингового события;regions - регионы, в которых будет проводиться рекламная кампания;start_dt - дата начала кампании;finish_dt - дата завершения кампании.Выведем основную информацию о датафрейме с помощью метода info().
ab_project_marketing_events.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes
Таблица ab_project_marketing_events содержит 4 столбца и 14 строк. Пропусков в данных нет. Обратим внимание, что столбцы start_dt и finish_dt содержат даты, а тип данных в этих столбцах - object.
Изучим данные таблицы final_ab_new_users, которая содержит всех пользователей, зарегистрировавшихся в интернет-магазине в период с 7 по 21 декабря 2020 года.
final_ab_new_users.tail(5)
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 61728 | 1DB53B933257165D | 2020-12-20 | EU | Android |
| 61729 | 538643EB4527ED03 | 2020-12-20 | EU | Mac |
| 61730 | 7ADEE837D5D8CBBD | 2020-12-20 | EU | PC |
| 61731 | 1C7D23927835213F | 2020-12-20 | EU | iPhone |
| 61732 | 8F04273BB2860229 | 2020-12-20 | EU | Android |
Таблица final_ab_new_users содержит следующую информацию:
user_id - идентификатор пользователя;first_date - дата регистрации;region - регион пользователя;device - устройство, с которого происходила регистрация.Выведем основную информацию о датафрейме с помощью метода info().
final_ab_new_users.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB
Таблица final_ab_new_users содержит 4 столбца и 61733 строк. Пропусков в данных нет. Обратим внимание, что столбец first_date содержит дату, а тип данных в этом столбце - object.
Изучим данные таблицы final_ab_events, которая содержит все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
final_ab_events.sample(5)
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 220325 | 5280F7999905FE4C | 2020-12-23 15:19:03 | product_page | NaN |
| 86553 | E2CC1F7FA824E7D5 | 2020-12-16 06:36:37 | product_cart | NaN |
| 404589 | 1A22BCA466C914A6 | 2020-12-24 17:02:47 | login | NaN |
| 58183 | 831FE6AD2B5A38A9 | 2020-12-27 01:39:50 | purchase | 4.99 |
| 330590 | C2960D22C20E7130 | 2020-12-17 03:05:59 | login | NaN |
Таблица final_ab_events содержит следующую информацию:
user_id - идентификатор пользователя;event_dt - дата и время события;event_name - тип события;details - дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.Выведем основную информацию о датафрейме с помощью метода info().
final_ab_events.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB
Таблица final_ab_events содержит 4 столбца и 440317 строк. Есть пропуски в столбце details. Обратим внимание, что столбец event_dt содержит дату и время, а тип данных этого столбца - object.
Изучим данные таблицы final_ab_participants - содержит участников тестов.
final_ab_participants.sample(5)
| user_id | group | ab_test | |
|---|---|---|---|
| 14385 | 1A918D147A69E900 | A | interface_eu_test |
| 18164 | 2159AF0085D9D2F4 | B | interface_eu_test |
| 15023 | 7F05A9462DF1748F | B | interface_eu_test |
| 13143 | E0E6BAA9618EBA2E | B | interface_eu_test |
| 9175 | 2C02642C6F4F48CB | B | interface_eu_test |
Таблица final_ab_participants содержит следующую информацию:
user_id - идентификатор пользователя;ab_test - название теста;group - группа пользователя.Выведем основную информацию о датафрейме с помощью метода info().
final_ab_participants.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB
Таблица final_ab_events содержит 3 столбца и 18268 строк. Пропусков в данных нет.
Изучим типы данных в датафрейме ab_project_marketing_events.
ab_project_marketing_events.dtypes
name object regions object start_dt object finish_dt object dtype: object
Столбцы start_dt и finish_dt - дата начала и дата завершения кампании содержат тип object, изменим его на datetime .
ab_project_marketing_events['start_dt'] \
= pd.to_datetime(ab_project_marketing_events['start_dt'])
ab_project_marketing_events['finish_dt'] \
= pd.to_datetime(ab_project_marketing_events['finish_dt'])
# check
ab_project_marketing_events.dtypes
name object regions object start_dt datetime64[ns] finish_dt datetime64[ns] dtype: object
Проверим наши данные на наличие пропусков.
ab_project_marketing_events.isna().sum()
name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
Пропусков в данных нет.
Проверим данные на наличие дубликатов.
ab_project_marketing_events.duplicated().sum()
0
В таблице нет строк-дубликатов.
Изучим типы данных в датафрейме final_ab_new_users.
final_ab_new_users.dtypes
user_id object first_date object region object device object dtype: object
Столбец first_date - дата регистрации, содержит тип object, изменим его на datetime.
final_ab_new_users['first_date'] \
= pd.to_datetime(final_ab_new_users['first_date'])
# check
final_ab_new_users.dtypes
user_id object first_date datetime64[ns] region object device object dtype: object
Проверим наши данные на наличие пропусков.
final_ab_new_users.isna().sum()
user_id 0 first_date 0 region 0 device 0 dtype: int64
Пропусков в данных нет.
Проверим данные на наличие дубликатов.
final_ab_new_users.duplicated().sum()
0
В таблице нет строк-дубликатов.
Изучим типы данных в датафрейме final_ab_events.
final_ab_events.dtypes
user_id object event_dt object event_name object details float64 dtype: object
Столбец event_dt - дата и время события, содержит тип object, изменим его на datetime.
final_ab_events['event_dt'] = pd.to_datetime(final_ab_events['event_dt'])
# check
final_ab_new_users.dtypes
user_id object first_date datetime64[ns] region object device object dtype: object
Проверим наши данные на наличие пропусков.
final_ab_events.isna().sum()
user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
В столбце details - 377577 пропусков. Нам необходимо их исследовать и постараться установить их тип, чтобы понять, можем ли мы заполнить пропуски, и если можем, то понять, как лучше это сделать.
# сохраним данные с пропусками в столбце переменной "missing_values"
missing_values = final_ab_events[final_ab_events['details'].isnull()]
Столбец details - дополнительные данные о событии, связан со столбцом event_name - тип события. Выведем на экран уникальные значения (и их количество) столбца event_name таблиц missing_values и final_ab_events.
missing_values.event_name.value_counts()
login 189552 product_page 125563 product_cart 62462 Name: event_name, dtype: int64
final_ab_events.event_name.value_counts()
login 189552 product_page 125563 purchase 62740 product_cart 62462 Name: event_name, dtype: int64
Пропуски связаны с типами событий:
login - вход/логин;product_page - просмотр карточек товаров;product_cart - просмотры корзины.Оставим пропуски в столбце details без изменений, на наше дальнейшее исследование это не повлияет.
Проверим данные на наличие дубликатов.
final_ab_events.duplicated().sum()
0
В таблице нет строк-дубликатов.
Изучим типы данных в датафрейме final_ab_participants.
final_ab_participants.dtypes
user_id object group object ab_test object dtype: object
Содержимое всех столбцов соответствует их типу - object.
Проверим наши данные на наличие пропусков.
final_ab_participants.isna().sum()
user_id 0 group 0 ab_test 0 dtype: int64
Пропусков в данных нет.
Проверим данные на наличие дубликатов.
final_ab_participants.duplicated().sum()
0
В таблице нет строк-дубликатов.
Проверим, соответствуют ли данные таблиц A/B-теста требованиям технического задания.
Техническое задание
product_page;product_cart;purchase.Для начала проверим, что в таблице final_ab_new_users пользователи регистрировались с 7 по 21 декабря 2020 года.
print('Дата регистрации с', final_ab_new_users['first_date'].min(), 'по', \
final_ab_new_users['first_date'].max())
Дата регистрации с 2020-12-07 00:00:00 по 2020-12-23 00:00:00
Дата окончания регистрации новых пользователей не соответствует заявленной. Оставим только тех пользователей, которые были зарегистрированы до 21 декабря 2020 года.
final_ab_new_users = final_ab_new_users[final_ab_new_users['first_date'] <= "2020-12-21"]
# check
print('Дата регистрации с', final_ab_new_users['first_date'].min(), 'по', \
final_ab_new_users['first_date'].max())
Дата регистрации с 2020-12-07 00:00:00 по 2020-12-21 00:00:00
Проверим, что в таблице final_ab_events новые пользователи совершали события с 7 декабря 2020 по 4 января 2021 года.
print('Дата и время события с', final_ab_events['event_dt'].min(), 'по', \
final_ab_events['event_dt'].max())
Дата и время события с 2020-12-07 00:00:33 по 2020-12-30 23:36:33
Последнее событие было совершено 30 декабря 2020 года, вместо 4 января 2021 года.
Последовательно объединим таблицы final_ab_participants, final_ab_events и final_ab_new_users по столбцу user_id. Объединённые данные сохраним в таблице final_ab.
final_ab = pd.merge(final_ab_participants, final_ab_events, on='user_id')
final_ab.head()
| user_id | group | ab_test | event_dt | event_name | details | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | purchase | 4.99 |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:29 | product_cart | NaN |
| 3 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:57 | product_cart | NaN |
| 4 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | product_page | NaN |
final_ab = pd.merge(final_ab, final_ab_new_users, on='user_id')
final_ab.head()
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 | 2020-12-07 | EU | PC |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | purchase | 4.99 | 2020-12-07 | EU | PC |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:29 | product_cart | NaN | 2020-12-07 | EU | PC |
| 3 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:57 | product_cart | NaN | 2020-12-07 | EU | PC |
| 4 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | product_page | NaN | 2020-12-07 | EU | PC |
Мы получили сводную таблицу по A/B-тесту. Проверим, что данные соответствуют требованиям технического задания. Если данные не будут соответствовать, постараемся их привести к необходимым требованиям.
В нашу сводную таблицу final_ab должны были попасть пользователи, у которых значение user_id присутствуют одновременно во всех таблицах: final_ab_participants, final_ab_new_users и final_ab_events. Или другими словами, это пользователи, которые были зарегистрированы в интернет-магазине в период с 7 по 21 декабря 2020 года, совершали события и являлись участниками теста. При объединении таблиц мы ожидаем, что данные таким образом отфильтруются и количество участников теста сократится. Также в сводной таблице не должно быть пустых строк, они допустимы только в столбце details в котором уже были пропуски.
final_ab.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 100508 entries, 0 to 100507 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 100508 non-null object 1 group 100508 non-null object 2 ab_test 100508 non-null object 3 event_dt 100508 non-null datetime64[ns] 4 event_name 100508 non-null object 5 details 14539 non-null float64 6 first_date 100508 non-null datetime64[ns] 7 region 100508 non-null object 8 device 100508 non-null object dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 7.7+ MB
Так и есть, пропуски остались только в столбце details.
Проверим, сохранились ли все пользователи, которые участвовали в тесте.
final_ab_participants_users = final_ab_participants['user_id'].nunique()
final_ab_users = final_ab['user_id'].nunique()
abs_final_ab_participants_users_lost = final_ab_participants_users - final_ab_users
rel_final_ab_participants_users_lost = abs_final_ab_participants_users_lost * 100 / final_ab_participants_users
print('Количество участников -', final_ab_participants_users)
print('Количество участников после объединения таблиц -', final_ab_users)
print('Количество участников, которое мы потеряли после объединения таблиц в абсолютной величине -', \
abs_final_ab_participants_users_lost)
print('Количество участников, которое мы потеряли после объединения таблиц в относительной величине -', \
round(rel_final_ab_participants_users_lost), '%')
Количество участников - 16666 Количество участников после объединения таблиц - 12636 Количество участников, которое мы потеряли после объединения таблиц в абсолютной величине - 4030 Количество участников, которое мы потеряли после объединения таблиц в относительной величине - 24 %
При объединени таблиц final_ab_participants, final_ab_events и final_ab_new_users по столбцу user_id мы потеряли 4030 человек или 24% от исходного количества человек, участвовавших в тесте. Таблицы были объединены корректно, так как количество участников в тесте сократилось и в сводной таблице нет пустых строк, кроме столбца details.
Проверим, что в таблицу final_ab попали только пользователи recommender_system_test.
# final_ab.ab_test.value_counts()
final_ab.ab_test.unique()
array(['recommender_system_test', 'interface_eu_test'], dtype=object)
Не соответствует требованиям технического задания.
В нашу сводную таблицу попали пользователи нашего теста - recommender_system_test и конкурирующего теста interface_eu_test.
Оставим только тех пользователей, которые попали в наш тест - recommender_system_test.
final_ab = final_ab[final_ab['ab_test'] == 'recommender_system_test']
final_ab.ab_test.unique()
array(['recommender_system_test'], dtype=object)
В нашей таблице остались только пользователи теста recommender_system_test.
final_ab.group.unique()
array(['A', 'B'], dtype=object)
В нашем тесте участвуют только две группы: А - контрольная и B - новая платёжная воронка.
# check
final_ab.first_date.min()
Timestamp('2020-12-07 00:00:00')
Дата запуска теста соответствует заявленной дате - 7 декабря 2020 года.
# check
final_ab.first_date.max()
Timestamp('2020-12-21 00:00:00')
Дата остановки набора новых пользователей соответствуют заявленной - 21 декабря 2020 года.
У нас есть столбец event_dt который содержит дату и время события, добавим столбец event_date, который будет содержать только дату события.
final_ab['event_date'] = pd.to_datetime(final_ab['event_dt']).dt.date
final_ab['event_date'] = pd.to_datetime(final_ab['event_date'])
final_ab.event_date.head(3)
0 2020-12-07 1 2020-12-25 2 2020-12-07 Name: event_date, dtype: datetime64[ns]
# check
print('Дата остановки теста', final_ab.event_date.max())
Дата остановки теста 2020-12-30 00:00:00
Не соответствует требованиям технического задания.
В этом пункте, нам необходимо проверить, что пользователи совершили событие в течение 14 дней с момента регистрации.
final_ab['day'] = final_ab['event_date'] - final_ab['first_date']
final_ab.head(2)
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | event_date | day | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 | 2020-12-07 | EU | PC | 2020-12-07 | 0 days |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-25 00:04:56 | purchase | 4.99 | 2020-12-07 | EU | PC | 2020-12-25 | 18 days |
Оставим только тех пользователей, которые совершили событие в течение 14 дней.
final_ab = final_ab[final_ab['day'] <= '14']
# check
final_ab[final_ab['day'] > '14']
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | event_date | day |
|---|
У нас остались только пользователи, которые совершили событие в течение 14 дней после регистрации.
Все пользователи, которые попали в наш тест должны быть из региона EU. Посмотрим, так ли это на самом деле:
final_ab.groupby('region').agg(user_id=('user_id', 'nunique'))
| user_id | |
|---|---|
| region | |
| APAC | 45 |
| CIS | 29 |
| EU | 3467 |
| N.America | 116 |
Оставим только пользователей из региона EU.
final_ab = final_ab[final_ab['region'] == 'EU']
# check
final_ab.groupby('region').agg(user_id=('user_id', 'nunique'))
| user_id | |
|---|---|
| region | |
| EU | 3467 |
Проверим аудитория теста. Для этого посчитаем количество новых пользователей из EU в таблице final_ab_new_users и тех пользователей, которые попали в наш тест таблицы final_ab_participants. Для того, чтобы посчитать пользователей из EU, которые попали в наш тест, объединим таблицы final_ab_participants и final_ab_new_users по user_id.
rec_users = (pd.merge(final_ab_participants, final_ab_new_users, on='user_id')
.query('ab_test == "recommender_system_test"')
.reset_index(drop=True))
rec_users.head()
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
| 3 | 04988C5DF189632E | A | recommender_system_test | 2020-12-14 | EU | iPhone |
| 4 | 482F14783456D21B | B | recommender_system_test | 2020-12-14 | EU | PC |
# количество новых пользователей из Европы
audience_EU_new_users = final_ab_new_users[final_ab_new_users['region'] == 'EU']['user_id'].nunique()
# количество пользователей из региона 'EU', которые попали в тест
rec_users_EU = rec_users[rec_users['region'] == 'EU']['user_id'].nunique()
# доля новых пользователей из региона EU, которые попали в тест
share_audience_EU = rec_users_EU * 100 / audience_EU_new_users
print('Доля новых пользователей из региона EU, которая попала в тест -', round(share_audience_EU), '%')
Доля новых пользователей из региона EU, которая попала в тест - 15 %
Соответствует требованиям технического задания.
Проверим, какое количество пользователей участвует в нашем тесте.
final_ab['user_id'].nunique()
3467
Не соответствует требованиям технического задания.
Поскольку мы фильтровали данные таблицы final_ab и некоторые строки были удалены, нам необходимо обновить индексы в итоговой таблице.
final_ab = final_ab.reset_index(drop=True)
final_ab.tail(3)
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | event_date | day | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 7716 | 80712ED4EA1B52A5 | A | recommender_system_test | 2020-12-14 05:48:50 | product_cart | NaN | 2020-12-14 | EU | Android | 2020-12-14 | 0 days |
| 7717 | 80712ED4EA1B52A5 | A | recommender_system_test | 2020-12-14 05:48:51 | product_page | NaN | 2020-12-14 | EU | Android | 2020-12-14 | 0 days |
| 7718 | 80712ED4EA1B52A5 | A | recommender_system_test | 2020-12-14 05:48:50 | login | NaN | 2020-12-14 | EU | Android | 2020-12-14 | 0 days |
Вывод: мы выявили пункты, которые не соответствовали требованиям ТЗ:
recommender_system_test были данные другого тетста - interface_eu_test.final_ab_new_users были пользователи, зарегистрировавшиеся в интернет-магазине в до 23 декабря 2020 года;EU.После приведения данных к требованиям ТЗ и фильтрации, у нас остались пункты, которые не соответствуют требованиям ТЗ:
Пунк ТЗ - ожидаемый эффект: пользователи покажут улучшение каждой метрики не менее, чем на 10% - проверим на этапе исследовательского анализа данных.
Проверим, что во время проведения нашего теста - с 4 по 30 декабря 2020 года, не было маркетинговых или других активностей. Для этого отфильтруем данные таблицы ab_project_marketing_events по дате и региону.
ab_project_marketing_events.head(2)
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
ab_project_marketing_events[((ab_project_marketing_events['start_dt'] <= '2020-12-30') &
(ab_project_marketing_events['finish_dt']>= '2020-12-07')) &
(ab_project_marketing_events['regions'].str.contains('EU'))
]
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
Мы обнаружили событие Christmas&New Year Promo, которое проходило во время нашего теста, в период с 25 декабря 2020 года по 3 января 2021 года. Период времени с 25 по 30 декабря 2020 года мог повлиять на наш тест.
Удостоверимся, что у нас нет пересечений с конкурирующим тестом - interface_eu_test, т.е. у нас нет пользователей, которые одновременно попали и в тест interface_eu_test, и в тест recommender_system_test. Для этого создадим таблицу ab_interface_eu_test в котором будут пользователи конкурирующего теста.
# создадим таблицу "ab_interface_eu_test", объединив последовательно таблицы "final_ab_participants",
# "final_ab_events" и "final_ab_new_users"
ab_interface_eu_test = (final_ab_participants
.query('ab_test == "interface_eu_test"')
.merge(final_ab_events, on='user_id'))
ab_interface_eu_test = ab_interface_eu_test.merge(final_ab_new_users, on='user_id')
ab_interface_eu_test.sample(5)
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | |
|---|---|---|---|---|---|---|---|---|---|
| 50050 | 9F4B06ED55DD7459 | A | interface_eu_test | 2020-12-14 21:57:04 | login | NaN | 2020-12-14 | EU | Android |
| 70508 | 9E96DBC540A55B2D | B | interface_eu_test | 2020-12-20 22:02:20 | purchase | 4.99 | 2020-12-10 | EU | Mac |
| 68534 | 7AD5F7DF4F7851B7 | B | interface_eu_test | 2020-12-13 23:21:38 | product_cart | NaN | 2020-12-07 | EU | PC |
| 53364 | 9F8CA77BEE0122C4 | B | interface_eu_test | 2020-12-18 19:46:50 | login | NaN | 2020-12-13 | EU | Android |
| 38593 | 646C7F1F2E01D218 | B | interface_eu_test | 2020-12-18 23:56:02 | purchase | 4.99 | 2020-12-17 | EU | iPhone |
# найдем пользователей, которые учавствовали в тесте "interface_eu_test" и "recommender_system_test"
# сохраним таких пользователей в таблице "two_test_users"
two_test_users = ab_interface_eu_test[ab_interface_eu_test['user_id'].isin(final_ab['user_id'])]
two_test_users.reset_index(drop=True).head()
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | FB6F6BC119E1DBD5 | B | interface_eu_test | 2020-12-18 05:10:22 | product_page | NaN | 2020-12-18 | EU | Android |
| 1 | FB6F6BC119E1DBD5 | B | interface_eu_test | 2020-12-24 00:41:26 | product_page | NaN | 2020-12-18 | EU | Android |
| 2 | FB6F6BC119E1DBD5 | B | interface_eu_test | 2020-12-18 05:10:21 | login | NaN | 2020-12-18 | EU | Android |
| 3 | FB6F6BC119E1DBD5 | B | interface_eu_test | 2020-12-24 00:41:25 | login | NaN | 2020-12-18 | EU | Android |
| 4 | 055A4CD17A483B8E | A | interface_eu_test | 2020-12-19 13:28:31 | purchase | 9.99 | 2020-12-19 | EU | Android |
# узнаем какое колиичество уникальных пользователей попало в оба теста, и обе группы
two_test_users.groupby('group').agg({'user_id': 'nunique'})
| user_id | |
|---|---|
| group | |
| A | 456 |
| B | 429 |
Таким образом, у нас есть пользователи, которые участвовали в обоих тестах, и попали в группу A и B теста interface_eu_test. Проверим, что у нас нет пользователей, которые одновременно попали:
B;interface_eu_test группы B и recommender_system_test группы A.Если у нас есть одни и те же пользователи, которые попали в оба теста группы A, это не повлияет на наш тест, так как группа A является контрольной, и эти пользователи видят одно и то же.
# найдем пользователей, которые учавствовали в обоих тестах в группе "B"
two_test_B = ab_interface_eu_test[(ab_interface_eu_test['group'] == 'B') &
ab_interface_eu_test['user_id']
.isin(final_ab[final_ab['group'] == 'B']['user_id'])]
two_test_B['user_id'].nunique()
104
Мы нашли пользователей, которые участвовали в обоих теста в группе B. Удалим таких пользователей из нашего теста.
final_ab = final_ab.drop(final_ab[final_ab['user_id'].isin(two_test_B['user_id'])].index)
# check
two_test_B = ab_interface_eu_test[(ab_interface_eu_test['group'] == 'B') &
ab_interface_eu_test['user_id']
.isin(final_ab[final_ab['group'] == 'B']['user_id'])]
two_test_B['user_id'].nunique()
0
# найдем пользователей, которые учавствовали в тесте `interface_eu_test` группы `B` и
# `recommender_system_test` группы `A`
int_B_rec_A = ab_interface_eu_test[(ab_interface_eu_test['group'] == 'B') &
ab_interface_eu_test['user_id']
.isin(final_ab[final_ab['group'] == 'A']['user_id'])]
int_B_rec_A['user_id'].nunique()
325
Мы обнаружили пользователей, которые участвовали в тесте interface_eu_test группы B и recommender_system_test группы A. Удалим таких пользователей из нашего теста.
final_ab = final_ab.drop(final_ab[final_ab['user_id'].isin(int_B_rec_A['user_id'])].index)
# check
int_B_rec_A = ab_interface_eu_test[(ab_interface_eu_test['group'] == 'B') &
ab_interface_eu_test['user_id']
.isin(final_ab[final_ab['group'] == 'A']['user_id'])]
int_B_rec_A['user_id'].nunique()
0
Проверим, что у нас остались только пользователи, которые попали в оба теста группы A.
# check
two_test_users = ab_interface_eu_test[ab_interface_eu_test['user_id'].isin(final_ab['user_id'])]
two_test_users.groupby('group').agg({'user_id': 'nunique'})
| user_id | |
|---|---|
| group | |
| A | 456 |
Мы удалили пользователей, которые одновременно попали в тест interface_eu_test группы B и наш тест recommender_system_test.
Теперь проверим, есть ли у нас пользователи, которые участвуют в двух группах теста одновременно.
final_ab.reset_index(drop=True).head(2)
| user_id | group | ab_test | event_dt | event_name | details | first_date | region | device | event_date | day | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:27 | purchase | 99.99 | 2020-12-07 | EU | PC | 2020-12-07 | 0 days |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 14:43:29 | product_cart | NaN | 2020-12-07 | EU | PC | 2020-12-07 | 0 days |
a_b_users = final_ab.groupby('user_id').agg({'group': ['nunique', 'unique']})
a_b_users.columns = ['groups', 'group_name']
a_b_users = a_b_users.query('groups > 1')
print('Количество пользователей в группе A:', final_ab.query('group == "A"')['user_id'].nunique())
print('Количество пользователей в группе B:', final_ab.query('group == "B"')['user_id'].nunique())
print('Количество пользователей, попавших обе группы:', len(a_b_users))
Количество пользователей в группе A: 2273 Количество пользователей в группе B: 765 Количество пользователей, попавших обе группы: 0
В нашем тесте recommender_system_test нет пользователей, которые участвуют в двух группах одновременно.
Проверим некоторые пункты ТЗ, которые могли измениться после удаления пользователей в сводной таблице final_ab.
# проверим долю новых пользователей из региона 'EU', которые попали в тест
# количество пользователей из региона 'EU', которые попали в тест
test_audience_EU = final_ab[final_ab['region'] == 'EU']['user_id'].nunique()
# доля новых пользователей из региона EU, которые попали в тест
share_audience_EU = test_audience_EU * 100 / audience_EU_new_users
print('Дата запуска теста:', final_ab.first_date.min())
print('Дата остановки набора новых пользователей:', final_ab.first_date.max())
print('Дата остановки теста:', final_ab.event_dt.max())
# print('Аудитория: доля новых пользователей из региона EU, которая попала в тест -', round(share_audience_EU), '%')
print('Ожидаемое количество участников теста:', final_ab['user_id'].nunique())
Дата запуска теста: 2020-12-07 00:00:00 Дата остановки набора новых пользователей: 2020-12-21 00:00:00 Дата остановки теста: 2020-12-21 23:58:57 Ожидаемое количество участников теста: 3038
Изменились следующие пункты ТЗ:
Мы видим, что наш тест остановился 21 декабря 2020 года, теперь мы можем сказать, что маркетинговое событие Christmas&New Year Promo, не повлияло на наш тест, т.к. Christmas&New Year Promo началось 25 декабря 2020 года.
Узнаем среднее количество событий на одного пользователя в группах A и B.
events_per_user = final_ab.groupby('group').agg({'event_name': 'count',
'user_id': 'nunique'})
events_per_user['avg_event'] = round(events_per_user['event_name'] / events_per_user['user_id'])
events_per_user
| event_name | user_id | avg_event | |
|---|---|---|---|
| group | |||
| A | 5163 | 2273 | 2.0 |
| B | 1606 | 765 | 2.0 |
Мы видим, что у нас разное количество участников в группах: в группе A участников в 3 раза больше, чем в группе B. Среднее количество событий на пользователя в грппах A и B одинаково - 2 события на одного пользователя.
# соберем данные о количестве событий на каждого пользователя для группы 'A' и группы 'B'
events_per_user = final_ab.pivot_table(index=['user_id', 'group'], values='event_name', aggfunc='count')
events_per_user.head()
| event_name | ||
|---|---|---|
| user_id | group | |
| 0010A1C096941592 | A | 3 |
| 00341D8401F0F665 | A | 1 |
| 003DF44D7589BBD4 | A | 3 |
| 00505E15A9D81546 | A | 1 |
| 005E096DBD379BCF | B | 2 |
# строим гистограмму
fig = px.histogram(events_per_user.reset_index(),
x='event_name',
color='group',
histnorm='probability density',
barmode ='overlay')
# оформляем график
fig.update_layout(title='Распределение событий на пользователя в группе A и B',
xaxis_title='Количество событий',
yaxis_title='Плотность распределения',
legend_title='Группа')
fig.show()
На графике мы видим, что в группе B пользователи чаще совершали по одному событию, участники группы A чаще совершали по 3 события. Практически с одинаковой частотой участники группы A и B совершали по 2 события. В обеих группах реже всего совершали по 4 события, в группе A участники немного чаще совершали по 4 события.
Посмотрим на распределение количества событий, совершенных пользователями за весь период. Для этого создадим сводную таблицу events_per_day и сгруппируем данные по столбу event_date.
events_per_day = final_ab.groupby(['group', 'event_date']).agg(count=('event_name', 'count'))
events_per_day.head()
| count | ||
|---|---|---|
| group | event_date | |
| A | 2020-12-07 | 263 |
| 2020-12-08 | 176 | |
| 2020-12-09 | 154 | |
| 2020-12-10 | 108 | |
| 2020-12-11 | 157 |
events_per_day = events_per_day.reset_index()
events_per_day_A = round(events_per_day.loc[events_per_day['group'] == 'A', 'count'].mean())
events_per_day_B = round(events_per_day.loc[events_per_day['group'] == 'B', 'count'].mean())
print('Среднее количество событий в день группы A -', events_per_day_A)
print('Среднее количество событий в день группы B -', events_per_day_B)
Среднее количество событий в день группы A - 344 Среднее количество событий в день группы B - 107
# визуализируем данные таблицы 'events_per_day'
fig = px.bar(events_per_day.reset_index(),
x='event_date',
y='count',
color='group',
barmode ='group')
# оформляем график
fig.update_layout(title='Распределение событий по дням в группе A и B',
xaxis_title='Дата',
yaxis_title='Плотность распределения',
legend_title='Группа')
fig.show()
На графике, мы видим, что в группе A с 7 по 13 декабря количество событий было небольшим от 88 до 263. С 14 декабря мы наблюдаем всплеск событий - 733, затем небольшой спад до 16 декабря - 331, и снова постепенный рост до 20 декабря - 529 событий, самое большое количество событий в группе A произошло в последний день теста - 21 декабря - 783 события.
В группе B на протяжении всего времени проведения теста, мы наблюдаем небольшое количество событий, по сравнению с группой A. Самое большое количество событий в группе B произошло в первый день теста - 315, самое маленькое количество событий произошло 13 декабря - всего 21 событие.
Обратим внимание, что среднее количество событий в день, которые совершают пользователи, в группе A - 344, почти в три раза больше, чем в группе B - 107 событий в день.
Посмотрим на общее распределение событий в тесте.
final_ab['event_dt'].hist(bins=14*24, figsize=(14, 5))
plt.minorticks_on()
plt.grid(which='minor', color='#aaa', ls=':')
plt.ylabel('Количество событий', fontsize = 12)
plt.xlabel('Дата', fontsize = 12)
plt.title('Распределение количества событий обеих групп теста по дням', fontsize = 14);
plt.show()
Общий график распределения событий по дням, напоминает график распределения событий группы A. Мы наблюдаем три пика по количеству событий: небольшой пик событий в первый день теста - 7 декабря 2020 года, пик событий 14 декабря и пик в последний день теста - 21 декабря 2020 года.
Узнаем какое количество пользователей в группах A и B совершали каждое из событий.
event_name_users = (final_ab
.pivot_table(index='event_name', columns='group', values='user_id', aggfunc='nunique'))
event_name_users.columns = ['A', 'B']
event_name_users
| A | B | |
|---|---|---|
| event_name | ||
| login | 2273 | 759 |
| product_cart | 685 | 210 |
| product_page | 1474 | 421 |
| purchase | 731 | 216 |
У нас есть следующие события:
login - вход/логин;product_cart- просмотры корзины;product_page - просмотр карточек товаров;purchase - покупки.Предположим порядок в котором происходят события:
login - вход/логин;product_page - просмотр карточек товаров;product_cart - просмотры корзины;purchase - покупки.Для корректного расчёта воронки в группах, расположим индексы событий в том порядке, в котором они происходят.
event_name_users = event_name_users.reset_index().reindex([0,2,1,3]).reset_index(drop=True)
event_name_users
| event_name | A | B | |
|---|---|---|---|
| 0 | login | 2273 | 759 |
| 1 | product_page | 1474 | 421 |
| 2 | product_cart | 685 | 210 |
| 3 | purchase | 731 | 216 |
Обратим внимание, что в количество пользователей, которые совершают заключительный шаг purchase - покупки, больше числа тех, кто находится на предыдущем шаге product_cart - просмотр корзины. Это можно объяснить тем, что корзина не является обязательным этапом воронки, например это могут быть покупки в один клик.
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'Группа A',
y = ['login', 'product_page', 'product_cart', 'purchase'],
x = event_name_users['A'],
textinfo = "value+percent initial"))
fig.add_trace(go.Funnel(
name = 'Группа B',
y = ['login', 'product_page', 'product_cart', 'purchase'],
x = event_name_users['B'],
textinfo = "value+percent initial"))
fig.update_layout(title='Конверсия в воронке событий группы A и B',
xaxis_title='')
fig.show()
У нас есть пунк в ТЗ: 1) Ожидаемый эффект: за 14 дней с момента регистрации в системе пользователи покажут улучшение каждой метрики не менее, чем на 10%:
product_page;product_cart;purchase. Добавим в таблицу event_name_users значения конверсий группы A и B, а затем расчитаем какого эффекта мы достигли - столбец exp_res.
event_name_users['conv_A, %'] = round(event_name_users['A'] * 100 / event_name_users['A'].shift(), 2)
event_name_users['conv_B, %'] = round(event_name_users['B'] * 100 / event_name_users['B'].shift(), 2)
event_name_users['exp_res'] = round((event_name_users['conv_B, %'] - event_name_users['conv_A, %'])*100 \
/ event_name_users['conv_A, %'], 2)
event_name_users
| event_name | A | B | conv_A, % | conv_B, % | exp_res | |
|---|---|---|---|---|---|---|
| 0 | login | 2273 | 759 | NaN | NaN | NaN |
| 1 | product_page | 1474 | 421 | 64.85 | 55.47 | -14.46 |
| 2 | product_cart | 685 | 210 | 46.47 | 49.88 | 7.34 |
| 3 | purchase | 731 | 216 | 106.72 | 102.86 | -3.62 |
Мы получили следующие результаты теста:
product_page ухудшилась на 14.46%;product_cart улучшилось на 7.34%;purchase ухудшилась на 3.62%.Ожидаемого эффекта мы не достигли, более того, тест показал ухудшение метрик product_page и purchase.
Данные, на которые необходимо обратить внимание при проведении A/B-тестирования:
Выделим параметры, которые могли негативно отразиться на результатах A/B-теста.
1) Вместе с нашим тестом recommender_system_test - рекомендательной системы, проходил конкурирующий тест interface_eu_test - изменение интерфейса, который мог повлиять на конверсию групп в воронке событий;
2) Мы обнаружили пользователей, которые участвовали в тесте interface_eu_test группы A и в тесте recommender_system_test группы B, interface_eu_test группы B и в тесте recommender_system_test группы A, interface_eu_test группы B и в тесте recommender_system_test группы B;
3) Набор новых пользователей был остановлен позже заявленного срока - 23 декабря 2020 года, вместо 21 декабря 2020 года;
4) Тест был остановлен на 6 дней раньше - 30 декабря 2020 года. После привидения данных к ТЗ и фильтрации, последний день проведения теста - 21 декабря 2020 года, таким образом длительность теста сократилась вдвое. Заявленный период проведения теста: с 7 декабря 2020 года по 4 января 2021 года, фактический период: с 7 по 21 декабря 2020 года;
5) Тест был ориенторован на пользователей из региона EU, однако в него попали пользователи из других регионов. После привидения данных к ТЗ и фильтрации, доля новых пользователй из EU составила 7%, вместо заявленных 15%;
6) Размер выборки сократился почти в два раза - 2922 участника, вместо ожидаемого количества - 6000 человек;
7) В нашем тесте произошло некорректное деление трафика: размер контрольной группы A в 3.5 раза больше экспериментальной группы B.
8) Ожидаемый эффект теста не был достигнут: пользователи не показали улучшение метрик на 10%.
Учитывая все вышеперечисленные факторы, мы не можем считать проведение A/B-теста успешным. Результаты теста являются ненадежными.
Для проведения A/B-теста построим воронку событий для контрольной группы A и экспериментальной группой B.
a_b_test = final_ab.pivot_table(index='event_name', columns='group', values='user_id', aggfunc='nunique')
a_b_test.columns = ['A', 'B']
a_b_test
| A | B | |
|---|---|---|
| event_name | ||
| login | 2273 | 759 |
| product_cart | 685 | 210 |
| product_page | 1474 | 421 |
| purchase | 731 | 216 |
Теперь выведем на экран количество участников группы A и B.
a_b_test_users = final_ab.pivot_table(index='group', values='user_id', aggfunc='nunique')
a_b_test_users
| user_id | |
|---|---|
| group | |
| A | 2273 |
| B | 765 |
Проверим равенство долей для каждого события между группами A и B. Разница между пропорциями, наблюдаемыми на выборках, будет нашей статистикой. Проверим гипотезу о равенстве долей между контрольной группой A и экспериментальной группой B.
Сформулируем нулевую и альтернативную гипотезу:
H_0: Доля пользователей события "login" в группе A = доля пользователей события "login" в группе B
H_a: Доля пользователей события "login" в группе A ≠ доля пользователей в события "login" группе B
# критический уровень статистической значимости
alpha = .05
successes = np.array([a_b_test['A'].iloc[0], a_b_test['B'].iloc[0]])
trials = np.array([a_b_test_users['user_id'].iloc[0], a_b_test_users['user_id'].iloc[1]])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = abs(p1 - p2)
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
p-значение: 2.3742571078289032e-05 Отвергаем нулевую гипотезу: между долями есть значимая разница
Отвергаем нулевую гипотезу, доли пользователей, которые совершают событие login в группе A и B статистически значимо различаются.
Нам необходимо проверить равенство долей пользователей для каждого события между группами A и B. Обернем проверку в функцию get_z_value.
def get_z_value(successes, trials, alpha):
p1 = successes[0]/trials[0]
p2 = successes[1]/trials[1]
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение:', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
В тесте нам необходимо попарно сравнить между собой 4 события: login, product_page, product_cart, purchase. Таким образом, мы проверим 4 гипотезы - проведём множественную проверку гипотез. Важная особенность множественной проверки в том, что с каждой новой проверкой гипотезы растёт вероятность ошибки первого рода, то есть мы можем найти различия там, где их нет. Чтобы снизить вероятность ложнопозитивного результата при множественном тестировании гипотез, применяют поправку на множественное сравнение, например можно использовать поправку Бонферрони: если мы хотим удержать вероятность ошибки первого рода на уровне 0.05, то мы должны alpha разделить на число гипотез:
alpha_exp = alpha / 4
print(alpha_exp)
0.0125
В нашем случае, чтобы снизить вероятность ошибки первого рода, мы должны применить уровень значимости alpha равным 0.0125.
H_0: Доля пользователей в группе A = доля пользователей в группе B
H_a: Доля пользователей в группе A ≠ доля пользователей в группе B
alpha = 0.0125
for event_name in a_b_test.index:
successes = np.array([a_b_test.loc[event_name, 'A'], a_b_test.loc[event_name, 'B']])
trials = np.array([a_b_test_users['user_id'].iloc[0], a_b_test_users['user_id'].iloc[1]])
print('Проверка равенства долей события', event_name, 'группы A и B')
get_z_value(successes, trials, alpha)
print('')
Проверка равенства долей события login группы A и B p-значение: 2.3742571078289032e-05 Отвергаем нулевую гипотезу: между долями есть значимая разница Проверка равенства долей события product_cart группы A и B p-значение: 0.15873902266782336 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Проверка равенства долей события product_page группы A и B p-значение: 1.2507690112517622e-06 Отвергаем нулевую гипотезу: между долями есть значимая разница Проверка равенства долей события purchase группы A и B p-значение: 0.04264272125867552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Отвергнуть нулевую гипотезу не получилось в событиях product_cart - просмотр корзины и purchase - покупки, это значит, что доли пользователей, которые совершают события product_cart и purchase статистически значимо не различаются.
Отвергнуть нулевую гипотезу получилось в событиях login - логин/вход и product_page - просмотр карточек товаров, это значит, что доли пользователей, которые совершают события login и product_page статистически значимо различаются.
На этапе предобработки данных мы проделали следующую работу:
ab_project_marketing_events в столбцах start_dt - дата начала и finish_dt - дата завершения кампании на datetime;final_ab_new_users в столбце first_date - дата регистрации на datetime;final_ab_events в столбце event_dt - дата и время события на datetime.Провели оценку корректности проведения теста:
В тесте recommender_system_test нет пользователей, которые участвовали в группе A и B одновременно.
После удаления пользователей в нашем тесте, изменились следующие пункты ТЗ:
Маркетинговое событие Christmas&New Year Promo, не повлияло на наш тест - recommender_system_test закончился раньше - 21 декабря 2020 года, чем началось Christmas&New Year Promo - 25 декабря 2020 года.
Провели исследовательский анализ:
A - 2273 человека;B - 765 человек.Количество пользователей в группе A в 3 раза больше, чем в группе B.
A - 344 события;B - 107 событий.Группа A:
product_page - 64.85%;product_cart - 46.47%;purchase - 106.72%. Группа B:
product_page - 55.47%; product_cart - 49.88%; purchase - 102.86%.За 14 дней с момента регистрации в системе пользователи показали следующие результаты:
product_page ухудшилась на 14.46%;product_cart улучшилось на 7.34%;purchase ухудшилась на 3.62%.Пунк ТЗ - ожидаемый эффект: за 14 дней с момента регистрации в системе пользователи покажут улучшение каждой метрики не менее, чем на 10% - достигнуть не удалось.
На этапе оценки результатов A/B-тестирования:
1) Выделили параметры, которые могли негативно отразиться на результатах A/B-теста. На основании этих параметров, мы не можем считать проведение A/B-теста успешным. Результаты теста являются ненадежными.
2) Провели анализ A/В-теста при уровне значимости 0,0125 и установили, что:
product_cart и purchase в контрольной группе A и экспериментальной группе B статистически значимо не различаются.login и product_page в контрольной группе A и экспериментальной группе B статистически значимо различаются.Рекомендуем признать проведение A/B-теста неудовлетворительным и провести тест recommender_system_test заново, с учётом выявленных ошибок.